(本篇文章網誌版:http://shineright.blogspot.tw/2016/12/day-28.html)
增加遊戲可玩性的方法有很多種,其中最簡單的方式就是加入排行榜機制,讓玩家有破紀錄、比較成續的快感。
今天我要為遊戲加入一個簡單的排行榜。這個排行榜可紀錄在該裝置中的前三高分,以及前三高分所使用的角色。
(示意圖)
在Start Scene中建立一個新的Game Object,並為它新增一段名為「ScoreBoardDataControl.cs」的Script。這段Script會比較複雜,因為排行榜機制牽涉到把資料序列化(Serialize)儲存至裝置,以及把裝燈的資料反序列化(Deserialize)變回程式可理解的物件。下圖是該段程式的流程簡圖。
(反序列化是Deserialize,圖片裡忘了改)
馬上進入程式碼:
using UnityEngine;
using System.Collections;
using System; //Serializable
using System.Runtime.Serialization.Formatters.Binary; //BinaryFormatter
using System.IO; //FileStream
public class ScoreBoardDataControl : MonoBehaviour
{
//Singleton Pattern,可使任何程式呼叫ScoreBoardDataControl.instance取得該物件
public static ScoreBoardDataControl instance;
//實際儲存分數資料的物件(ScoreData定義在下方)
private ScoreData data;
//要顯示的名次數量(3表示只顯示前三名)
private const int Places = 3;
void Awake()
{
//Singleton Pattern,只能擁有一個ScoreBoardDataControl物件
if (instance == null) {
//轉換場景時不要移除此Game Object
DontDestroyOnLoad (gameObject);
//載入排行榜資料
LoadData ();
//把instance設好,以供其他地方以ScoreBoardDataControl.instance取得物件
instance = this;
} else if (instance != this) {
//第二次進入開始畫面,把新的ScoreBoardDataControl刪掉
Destroy (gameObject);
}
}
void LoadData()
{
//如果檔案存在(表示不是第一次開啟遊戲)
if (File.Exists (Application.persistentDataPath + "/scoreInfo.dat")) {
BinaryFormatter bf = new BinaryFormatter ();
FileStream file = File.Open (Application.persistentDataPath + "/scoreInfo.dat", FileMode.Open);
//把裝置中的二進位檔案反序列化,存入data變數中
data = (ScoreData) bf.Deserialize (file);
file.Close ();
} else { //如果檔案不存在(第一次開啟遊戲)
InitData (); //初始化資料
SaveData (); //儲入裝置
}
}
void InitData()
{
//初始化data、data的陣列
data = new ScoreData ();
data.scores = new int[Places];
data.playerTypes = new int[Places];
for (int i = 0; i < Places; i++) {
data.scores [i] = 0;
data.playerTypes [i] = -1;
}
}
void SaveData()
{
BinaryFormatter bf = new BinaryFormatter ();
//新增檔案
FileStream file = File.Create (Application.persistentDataPath + "/scoreInfo.dat");
//序列化,存入裝置
bf.Serialize (file, data);
file.Close ();
}
public void NewScore(int playerType, int score)
{
//判斷此分數能排在第幾名
int place = Places - 1;
while (place >= 0 && score > data.scores [place]) {
place--;
}
place++;
//無法進入排行榜
if (place >= Places)
return;
//把此分數之後的排名向後挪一名
for (int i = Places - 2; i >= place; i--) {
data.scores [i + 1] = data.scores [i];
data.playerTypes [i + 1] = data.playerTypes [i];
}
//更新名次
data.scores [place] = score;
data.playerTypes [place] = playerType;
//存入裝置
SaveData ();
}
//取得place名次的分數
public int GetScore(int place)
{
return data.scores [place];
}
//取得place名次所使用的角色
public int GetPlayerType(int place)
{
return data.playerTypes [place];
}
}
[Serializable] //可序列化的類別
class ScoreData
{
public int[] scores; //儲存前三名的分數
public int[] playerTypes; //儲存前三名使用的角色
}
大致解說一下上面的程式碼。首先,我使用了類似Singleton Pattern的設計模式(Design Pattern),使整個遊戲只允許一個ScoreBoardDataControl
物件存在,並在任何程式碼都可藉ScoreBoardDataControl.instance
取得該物件的參考(reference)。
Awake()
函數中,我判斷instance
是否已經存在,若已經存在,則把新的ScoreBoardDataControl
物件刪除,確保只有一個ScoreBoardDataControl
物件存在。若不存在,則把自己指定給instance
,並以DontDestroyOnLoad()
使該物件不會在轉換場景時被移除。
LoadData
和SaveData
把資料存入或取出裝置中的二進位檔案。其中的Application.persistentDataPath
是Unity依照不同作業系統定義的檔案儲存位置。
NewScore(int score)
之後會在遊戲結束時被呼叫,用以判斷新的分數是否足以登上排行榜。如果可以,則調整排行榜資料,並存入裝置。
GetScore
和GetPlayerType
則會在瀏覽排行榜時被呼叫,取得data
變數的資料。
最後,[Serializable]
屬性(Attribute)讓ScoreData
類別可被序列化,存入裝置。
寫完Script後,開個新的Canvas來佈置一下排行榜。
排行榜主要由三個Image和三個Text構成,分別代表前三名所使用的角色和分數。
為每一個Text加上下面的Script。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class ScoreboardScoreText : MonoBehaviour
{
//第幾名 (第一名:0,第二名:1,第三名:2)
public int place;
void Start()
{
//由ScoreBoardDataControl依照名次取得分數
int score = ScoreBoardDataControl.instance.GetScore (place);
//若分數不為零,更新分數。為零則不顯示。
if (score != 0) {
GetComponent<Text> ().text = score.ToString () + "cm";
} else {
gameObject.SetActive (false);
}
}
}
為每一個Image加入以下Script。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class ScoreboardIcon : MonoBehaviour
{
//名次
public int place;
//存儲各角色sprite的陣列
public Sprite[] characterTypes;
void Start()
{
//依照名次取得使用的角色
int charType = ScoreBoardDataControl.instance.GetPlayerType (place);
//如果不為-1,則根據角色更改Sprite。為-1(沒有該名次)則不顯示
if (charType != -1) {
GetComponent<Image> ().sprite = characterTypes [charType];
} else {
gameObject.SetActive (false);
}
}
}
在Inspector把角色的Sprite依照順序拉入Character Types陣列,並把Text和Image的Place變數設定好。
接著在GameOverCanvas.cs的Start()
函數加入
ScoreBoardDataControl.instance.NewScore (GameObject.FindObjectOfType<Character> ().charIndex, score);
來儲存該次遊戲結束的分數,排行榜就完成了!
在開始畫面和結束畫面新增一個開啟排行榜的按鈕,玩家就可觀看排行榜的成續了。
待續。